<?php

namespace app\modules\mall\models;

use Yii;

/**
 * 支付宝手机支付
 * @author XT
 *
 */
class Alipay {

    // 交易安全校验码
    public $key;

    // 请求参数配置，支付宝接口文档中所需的参数
    public $alipay_config=[];

    // HTTPS证书，用于cURL
    // 默认和本类文件同级目录的cacert.pem文件
    public $credential;

    public $notify_data = null;

    // 支付宝即时到账网关地址
    const ALIPAY_GATEWAY = 'https://mapi.alipay.com/gateway.do?';

    // HTTPS形式消息验证地址
    const HTTPS_VERIFY_URL = 'https://mapi.alipay.com/gateway.do?service=notify_verify&';

    // HTTP形式消息验证地址
    const HTTP_VERIFY_URL = 'http://notify.alipay.com/trade/notify_query.do?';

    // 移动网页支付网关
    const ALIPAY_PAGE_GATEWAY = 'http://wappaygw.alipay.com/service/rest.htm?';
    
    const SERVICE_WAP = 'alipay.wap.trade.create.direct';
    
    /**
     * 把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
     * @param $para 需要拼接的数组
     * return 拼接完成以后的字符串
     */
    private function createLinkstring($para) {
        $arg  = "";
        while (list ($key, $val) = each ($para)) {
            $arg.=$key."=".$val."&";
        }
        //去掉最后一个&字符
        $arg = substr($arg,0,count($arg)-2);
        //如果存在转义字符，那么去掉转义
        if(get_magic_quotes_gpc()){$arg = stripslashes($arg);}
        return $arg;
    }
    
    /**
     * 把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串，并对字符串做urlencode编码
     * @param $para 需要拼接的数组
     * return 拼接完成以后的字符串
     */
    private function createLinkstringUrlencode($para) {
        $arg  = "";
        while (list ($key, $val) = each ($para)) {
            $arg.=$key."=".urlencode($val)."&";
        }
        //去掉最后一个&字符
        $arg = substr($arg,0,count($arg)-2);
    
        //如果存在转义字符，那么去掉转义
        if(get_magic_quotes_gpc()){$arg = stripslashes($arg);}
    
        return $arg;
    }
    
    /**
     * 除去数组中的空值和签名参数
     * @param $para 签名参数组
     * return 去掉空值与签名参数后的新签名参数组
     */
    private function paraFilter($para) {
        $para_filter = array();
        while (list ($key, $val) = each ($para)) {
            if($key == "sign" || $key == "sign_type" || $val == "")continue;
            else	$para_filter[$key] = $para[$key];
        }
        return $para_filter;
    }
    
    /**
     * 对数组排序
     * @param $para 排序前的数组
     * return 排序后的数组
     */
    private function argSort($para) {
        ksort($para);
        reset($para);
        return $para;
    }
    
    /**
     * RSA签名
     * @param $data 待签名数据
     * @param $private_key_path 商户私钥文件路径
     * return 签名结果
     */
    private function rsaSign($data, $private_key_path) {
        $priKey = file_get_contents($private_key_path);
        $res = openssl_get_privatekey($priKey);
        openssl_sign($data, $sign, $res);
        openssl_free_key($res);
        //base64编码
        $sign = base64_encode($sign);
        return $sign;
    }
    
    /**
     * RSA验签
     * @param $data 待签名数据
     * @param $ali_public_key_path 支付宝的公钥文件路径
     * @param $sign 要校对的的签名结果
     * return 验证结果
     */
    private function rsaVerify($data, $ali_public_key_path, $sign)  {
        $pubKey = file_get_contents($ali_public_key_path);
        $res = openssl_get_publickey($pubKey);
        $result = (bool)openssl_verify($data, base64_decode($sign), $res);
        openssl_free_key($res);
        return $result;
    }
    
    /**
     * RSA解密
     * @param $content 需要解密的内容，密文
     * @param $private_key_path 商户私钥文件路径
     * return 解密后内容，明文
     */
    private function rsaDecrypt($content, $private_key_path) {
        $priKey = file_get_contents($private_key_path);
        $res = openssl_get_privatekey($priKey);
        //用base64将内容还原成二进制
        $content = base64_decode($content);
        //把需要解密的内容，按128位拆开解密
        $result  = '';
        for($i = 0; $i < strlen($content)/128; $i++  ) {
            $data = substr($content, $i * 128, 128);
            openssl_private_decrypt($data, $decrypt, $res);
            $result .= $decrypt;
        }
        openssl_free_key($res);
        return $result;
    }
    
    /**
     * 获取返回时的签名验证结果
     * @param $para_temp 通知返回来的参数数组
     * @param $sign 返回的签名结果
     * @return 签名验证结果
     */
    private function getSignVeryfy($para_temp, $sign) {
        //除去待签名参数数组中的空值和签名参数
        $para_filter = $this->paraFilter($para_temp);
    
        //对待签名参数数组排序
        $para_sort = $this->argSort($para_filter);
    
        //把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = $this->createLinkstring($para_sort);
        $isSgin = false;
        switch (strtoupper(trim($this->alipay_config['sign_type']))) {
            case "RSA" :
                $isSgin = $this->rsaVerify($prestr, trim($this->alipay_config['ali_public_key_path']), $sign);
                break;
            default :
                $isSgin = false;
        }
        return $isSgin;
    }
    
    /**
     * 针对notify_url验证消息是否是支付宝发出的合法消息
     * @return 验证结果
     */
    public function verifyNotify(){
        if(empty($_POST)) {
            return false;
        } else {
            //生成签名结果
            $isSign = $this->getSignVeryfy($_POST, $_POST["sign"]);
            //获取支付宝远程服务器ATN结果（验证是否是支付宝发来的消息）
            $responseTxt = 'true';
            if (! empty($_POST["notify_id"])) {
                $responseTxt = $this->getResponse($_POST["notify_id"]);
            }
            	
            //验证
            if (preg_match("/true$/i",$responseTxt) && $isSign) {
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * 获取远程服务器ATN结果,验证返回URL
     * @param $notify_id 通知校验ID
     * @return 服务器ATN结果
     * 验证结果集：
     * invalid命令参数不对 出现这个错误，请检测返回处理中partner和key是否为空
     * true 返回正确信息
     * false 请检查防火墙或者是服务器阻止端口问题以及验证时间是否超过一分钟
     */
    private function getResponse($notify_id) {
        $transport = strtolower(trim($this->alipay_config['transport']));
        $partner = trim($this->alipay_config['partner']);
        $veryfy_url = '';
        if($transport == 'https') {
            $veryfy_url = self::HTTPS_VERIFY_URL;
        }
        else {
            $veryfy_url = self::HTTP_VERIFY_URL;
        }
        $veryfy_url = $veryfy_url."partner=" . $partner . "&notify_id=" . $notify_id;
        $responseTxt = $this->getHttpResponseGET($veryfy_url, $this->alipay_config['cacert']);
    
        return $responseTxt;
    }
    
    /**
     * 远程获取数据，GET模式
     * 注意：
     * 1.使用Crul需要修改服务器中php.ini文件的设置，找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效，目前默认路径是：getcwd().'\\cacert.pem'
     * @param $url 指定URL完整路径地址
     * @param $cacert_url 指定当前工作目录绝对路径
     * return 远程输出的数据
     */
    private function getHttpResponseGET($url,$cacert_url) {
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$cacert_url);//证书地址
        $responseText = curl_exec($curl);
        curl_close($curl);
        return $responseText;
    }
    
    /**
     * 生成签名结果
     * @param $para_sort 已排序要签名的数组
     * return 签名结果字符串
     */
    private function buildRequestMysign($para_sort) {
        //把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = $this->createLinkstring($para_sort);
    
        $mysign = "";
        switch (strtoupper(trim($this->alipay_config['sign_type']))) {
            case "RSA" :
                $mysign = $this->rsaSign($prestr, $this->alipay_config['private_key_path']);
                break;
            default :
                $mysign = "";
        }
        return $mysign;
    }

    /**
     * 生成要请求给支付宝的参数数组
     * @param $para_temp 请求前的参数数组
     * @return 要请求的参数数组
     */
    private function buildRequestPara($para_temp) {
        //除去待签名参数数组中的空值和签名参数
        $para_filter = $this->paraFilter($para_temp);
    
        //对待签名参数数组排序
        $para_sort = $this->argSort($para_filter);
    
        //生成签名结果
        $mysign = $this->buildRequestMysign($para_sort);
    
        //签名结果与签名方式加入请求提交参数组中
        $para_sort['sign']      = $mysign;
        $para_sort['sign_type'] = strtoupper(trim($this->alipay_config['sign_type']));
    
        return $para_sort;
    }
    
    /**
     * 生成要请求给支付宝的参数数组
     * @param $para_temp 请求前的参数数组
     * @return 要请求的参数数组字符串
     */
    public function buildRequestParaToString($para_temp) {
        //待请求参数数组
        $para = $this->buildRequestPara($para_temp);
    
        //把参数组中所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串，并对字符串做urlencode编码
        $request_data = $this->createLinkstringUrlencode($para);
    
        return $request_data;
    }
    
    /**
     * 准备移动网页支付的请求参数
     *
     * 移动网页支付接口不同，需要先服务器提交一次请求，拿到返回 token 再返回客户端发起真实支付请求。
     * 该方法只完成第一次服务端请求，生成参数后需要客户端另行处理（可调用`buildRequestFormHTML`生成表单提交）。
     *
     * @param $params <Array>
     *        $params['out_trade_no'] 订单唯一编号
     *        $params['subject']      商品标题
     *        $params['total_fee']    支付总费用
     *        $params['merchant_url'] 商品链接地址
     *        $params['req_id']       请求唯一 ID
     *
     * @return <Array>
     */
    public function prepareMobileTradeData($params) {
        // 不要用 SimpleXML 来构建 xml 结构，因为有第一行文档申明支付宝验证不通过
        /*
        $xml_str = '<direct_trade_create_req>' .
            '<notify_url>' . $this->alipay_config['notify_url'] . '</notify_url>'.
            '<call_back_url>' . $this->alipay_config['return_url'] . '</call_back_url>'.
            '<seller_account_name>' . $this->alipay_config['seller_email'] . '</seller_account_name>'.
    
            '<out_trade_no>' . $params['out_trade_no'] . '</out_trade_no>'.
            '<subject>' . htmlspecialchars($params['subject'] , ENT_XML1, 'UTF-8') . '</subject>'.
            '<total_fee>' . $params['total_fee'] . '</total_fee>'.
            '<merchant_url>' . $params['merchant_url'] . '</merchant_url>' .
            '</direct_trade_create_req>';*/
        $xml_str = '<direct_trade_create_req>' .
            '<notify_url>http://www.baidu.com/</notify_url>'.
            '<call_back_url>http://www.baidu.com/</call_back_url>'.
            '<seller_account_name>' . $this->alipay_config['seller_id'] . '</seller_account_name>'.    
            '<out_trade_no>' . $params['out_trade_no'] . '</out_trade_no>'.
            '<subject>' . htmlspecialchars($params['subject'] , ENT_XML1, 'UTF-8') . '</subject>'.
            '<total_fee>' . $params['total_fee'] . '</total_fee>'.
            '<merchant_url>' . $params['merchant_url'] . '</merchant_url>' .
            '</direct_trade_create_req>';
        $request_data = $this->buildRequestPara(array(
            'service'           => self::SERVICE_WAP,
            'partner'           => $this->alipay_config['partner'],
            //'sec_id'            => $this->alipay_config['sign_type'],
            'sec_id'            => '0001',
            'format'            => 'xml',
            'v'                 => '2.0',
    
            'req_id'            => $params['req_id'],
            'req_data'          => $xml_str,
            	
            '_input_charset'    => $this->alipay_config['input_charset']
        ));
        $url = self::ALIPAY_PAGE_GATEWAY;
        $input_charset = trim(strtolower($this->alipay_config['input_charset']));
    
        if (trim($input_charset) != '') {
            $url = $url."_input_charset=".$input_charset;
        }
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO, $this->alipay_config['cacert']);//证书地址
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl, CURLOPT_POST, true); // post传输数据
        curl_setopt($curl, CURLOPT_POSTFIELDS, $request_data);// post传输数据
        $responseText = curl_exec($curl);
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);
        parse_str($responseText, $responseData);
        print_R($responseData);die;
        if( ! empty ($responseData['res_data'])) {
    
            if($this->alipay_config['sign_type'] == 'RSA') {
                $responseData['res_data'] = $this->rsaDecrypt($responseData['res_data'], $this->alipay_config['private_key_path']);
            }
    
            //token从res_data中解析出来（也就是说res_data中已经包含token的内容）
            $doc = new \DOMDocument();
            $doc->loadXML($responseData['res_data']);
            $responseData['request_token'] = $doc->getElementsByTagName( "request_token" )->item(0)->nodeValue;
        }
    
        $xml_str = '<auth_and_execute_req>'.
            '<request_token>' . $responseData['request_token'] . '</request_token>'.
            '</auth_and_execute_req>';
    
        return array(
            'service'           => 'alipay.wap.auth.authAndExecute',
            'partner'           => $this->alipay_config['partner'],
            'sec_id'            => $this->alipay_config['sign_type'],
            'format'            => 'xml',
            'v'                 => '2.0',
            'req_data'          => $xml_str
        );
    }
}